$KCODE = 'u'

require 'logger'
require 'kconv'
require 'yaml'

require 'rubygems'
require 'hpricot'
require 'ostruct'

module Rire
  VERSION = '0.0.1'

  class DSL
    def initialize(options = {})
      @options = options
      @plugin_dir = options[:plugin_dir] || File.expand_path(File.dirname($0)) + '/plugin'
      @plugin = Rire::Plugin.new.load(@plugin_dir)
    end

    def load(filename = 'rire.conf')
      instance_eval(File::open(filename, "r").read, filename, 1)
    end

    def define_task(&block)
      return nil if block.nil?

      context = Rire::Context.new(@plugin, @options)
      context.instance_eval(&block)
      context
    end
  end

  class Plugin
    def initialize
      @methods = {}
    end

    def load(dir)
      Dir::glob("#{dir}/**/*.rb") {|file|
        key = file.sub(%r{^#{dir}/(.+?)\.rb}, "\\1").gsub(%r{/}, ".").downcase
        @methods[key.to_sym] = Rire::Plugin::Method.new(file)

#       $stderr.puts "load plugin2: #{key}"
      }

      self
    end

    def send(name, *argv, &block)
      unless @methods.key?(name)
        $stderr.puts "not found plugin method: name=[#{name}]"
        return
      end

      @methods[name].send(name.to_s.sub(/.*\./, ""), *argv, &block)
    end

    def load?(name)
      @methods.key?(name)
    end

    def include?(name)
      @methods.keys.any? {|m| m.to_s =~ /^#{name}/}
    end

    class Method
      def initialize(filename)
        instance_eval(File::open(filename, "r").read, filename, 1)
      end
    end
  end

  class Context
    attr_accessor :entries, :log

    def initialize(plugin, options = {})
      @plugin = plugin
      @entries = []
      @stack = []
      @log = Logger.new(options[:log] || STDERR)
      @log.level = Logger::DEBUG
    end

    def method_missing(name, *argv, &block)
      @stack << name

      plugin_name = @stack.join('.').to_sym

      unless @plugin.load?(plugin_name)
        unless @plugin.include?(plugin_name)
          raise NameError, "undefined method `#{plugin_name}'"
        end
        return self
      end

      if argv.length > 0
        params = argv.select {|item| item.is_a?(Hash) && item.key?(:config)}
        config = params.inject({}) do |result, item|
          case item[:config]
          when Hash
            result.merge(item[:config])
          when Array
            item[:config]
          else
            result
          end
        end
      else
        config = {}
      end

      $stderr.puts "execute plugin: #{plugin_name}"

      @plugin.send(plugin_name, self, config, &block)

      @stack = []

      self
    end
  end

  class Config
    attr_accessor :config

    def initialize
      @config = {}
    end

    def self.parse(&block)
      r = nil
      config = Rire::Config.new
      r = config.instance_eval(&block)  unless block.nil?
      if r.is_a?(Array)
        r
      else
        config.config
      end
    end

    def method_missing(name, *argv, &block)
      unless block.nil?
        @config[name] = Rire::Config.parse &block
      else
        @config[name] = argv
      end
      @config[name] = @config[name][0]  if @config[name].is_a?(Array) && @config[name].length == 1

      name
    end
  end

  class Entry < Hash
    def method_missing(name, *argv)
      if name.to_s =~ /(.+?)=$/
        method_name = $1.to_sym
      else
        method_name = name
      end

      if argv.length > 0
        value = argv[0]
      else
        value = Rire::Entry.new
      end

      self[method_name] = value

      self.instance_eval <<-EOD
        def #{method_name}
          self[:#{method_name}]
        end
        def #{method_name}=value
          self[:#{method_name}] = value
        end
      EOD

      __send__(method_name)
    end
  end

  class Asset
    def initialize()
      @assets = {}
    end

    def load(path)
#     $stderr. puts "assets path: #{path}"

      Dir::glob(path + '/*.yaml') do |file|
        $stderr.puts "assets file: #{file}"
        @assets[File.basename(file)] = YAML.load(File.read(file).toutf8)
	  end
    end

    def select(&block)
      return nil  if block.nil?

      @assets.each_value do |asset|
        return asset  if asset.instance_eval(block)
	  end
      nil
    end
  end

  class PropertyBase < Array
    @@instances = {}

    def self.fetch(text)
      return {}  unless @@instances.key?(self)
      define = @@instances[self]

      if define.key?(:foreach)
        records = self.new

        text.each do |s|
          define[:foreach].call(s)

          struct = {}
          define[:has_property].each do |o|
            struct[o[:name]] = o[:options]
          end
          records << OpenStruct.new(struct)
        end

        records
      else
        if define[:options].key?(:xpath)
          records = Hpricot(text).search(define[:options][:xpath]).collect {|r| r.to_s}
        elsif define[:options].key?(:regexp)
          records = text.scan(%r{#{define[:options][:regexp]}}m).collect {|r| r.to_s}
        end

        entries = self.new

        define[:record].call

        records.each do |r|
          struct = {}
          define[:has_property].each do |o|
            struct[o[:name]] = get_property(r, o[:options])
          end
          entries << OpenStruct.new(struct)
		end

        entries.map! {|entry|
          r = entry
          r = define[:after_hook].call(entry)   if define.key?(:after_hook)
          r
        }.compact
      end
    end

    def self.foreach(&block)
      @@instances[self] = {}  unless @@instances.key?(self)
      @@instances[self][:foreach] = block
    end

    def self.has_property(name, options)
      @@instances[self] = {}  unless @@instances.key?(self)
      @@instances[self][:has_property] = [] unless @@instances[self].key?(:has_property)
      @@instances[self][:has_property] << {:name => name, :options => options}
    end

    def self.record(options, &block)
      @@instances[self] = {}  unless @@instances.key?(self)
      @@instances[self][:options] = options
      @@instances[self][:record] = block
    end

    def self.after_hook(&block)
      @@instances[self] = {}  unless @@instances.key?(self)
      @@instances[self][:after_hook] = block
    end

    # private methods
    def self.get_property(text, options)
      if options.key?(:xpath)
        doc = Hpricot(text)

        elem = doc.at(options[:xpath])
        case elem
        when Hpricot::Text
          elem.to_s
        when Hpricot::Elem
          case options[:expr]
          when /inner_text/
            elem.inner_text
          when /text/
            elem.search('/').map{|e| e if e.class == Hpricot::Text }.compact.join('')
          when /@(.+)/
            elem[$1]
          else
            elem.inner_text
          end
        else
          ''
        end
      elsif options.key?(:regexp)
        text.match(%r{#{options[:regexp]}}m).to_a[1]
      end
    end
    private_class_method :get_property

  end

end

